package edu.neumont.pro180._2009_q2;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Stack;

import edu.neumont.pro180._2009_q2.interfaces.ICommand;

/**
 * The Board populates itself with instances of Square, and provides
 * functionality for looking up Squares by their position on the board and their
 * position relative to other Squares.
 * 
 * @author Tyler Young
 */
public class Board {
	private int rowCount, columnCount;
	private Map<Square, Point> pointLookup = new HashMap<Square, Point>();
	private Map<Point, Square> squareLookup = new HashMap<Point, Square>();
	private ArrayList<Player> players = new ArrayList<Player>();
	private Player activePlayer;
	private Stack<ICommand> moveStack = new Stack<ICommand>();
	private Stack<ICommand> redoMoveStack = new Stack<ICommand>();

	/**
	 * A rectangular Board that will be populated with Squares.
	 * 
	 * @param rowCount
	 *            Number of rows the Board will have. Must be > 0.
	 * @param columnCount
	 *            Number of columns the Board will have. Must be > 0.
	 */
	public Board(int rowCount, int columnCount) {
		if (!(rowCount > 0 && columnCount > 0)) {
			throw new IllegalArgumentException(
					"Row and column totals must both be greater than zero!");
		}
		this.rowCount = rowCount;
		this.columnCount = columnCount;
		for (int x = 0; x < columnCount; ++x) {
			for (int y = 0; y < rowCount; ++y) {
				Point p = new Point(x, y);
				Square s = new Square(this, x, y);
				pointLookup.put(s, p);
				squareLookup.put(p, s);
			}
		}

	}

	/**
	 * Get the number of rows on the Board.
	 * 
	 * @return The number of rows on the board.
	 */
	public int getRowCount() {
		return this.rowCount;
	}

	/**
	 * Get the number of columns on the Board.
	 * 
	 * @return The number of columns on the board.
	 */
	public int getColumnCount() {
		return this.columnCount;
	}

	/**
	 * Get the square at a given point on the board. Returns null if the
	 * requested point is not on the board.
	 * 
	 * @param location
	 *            The point on the board that you're interested in.
	 * @return The square at the requested location, or null if there is no
	 *         square at that point (out of bounds).
	 */
	public Square getSquare(Point location) {
		return squareLookup.get(location);
	}
	
	public Point getPoint(Square location) {
		return pointLookup.get(location);
	}

	/**
	 * Get a Square at a given offset from the source Square.
	 * 
	 * @param source
	 *            The Square that is the starting point for the lookup.
	 * @param offset
	 *            The Point representing how far to travel in the x and y
	 *            directions to travel.
	 * @return A Square if there is one at that point, or null if the specified
	 *         point is off the board.
	 */
	public Square getNeighbor(Square source, Point offset) {
		Point origin = pointLookup.get(source);
		Point destination = new Point(origin.x + offset.x, origin.y + offset.y);
		return squareLookup.get(destination);
	}

	/**
	 * Gets the offset between two Squares
	 * 
	 * @param source
	 *            The first Square
	 * @param destination
	 *            The last Square
	 * @return The distance between the source and the destination Squares.
	 */
	public Point getOffset(Square source, Square destination) {
		Point pOrigin = this.pointLookup.get(source);
		Point pDestination = this.pointLookup.get(destination);
		Point offset = null;
		if ((null != pOrigin) && (null != pDestination)) {
			offset = new Point(pDestination.x - pOrigin.x, pDestination.y
					- pOrigin.y);
		}
		return offset;
	}

	/**
	 * Get a copy of the collection of players who are playing on this board.
	 * 
	 * @return a collection of players on the board.
	 */
	public Collection<Player> getPlayerCollection() {
		return new HashSet<Player>(this.players);
	}

	/**
	 * Adds the player to the collection of players on this board. Also sets this board as the player's board.
	 * @param the player to add to the player collection.
	 */
	public void addPlayer(Player player) {
		if (!players.contains(player)) {
			players.add(player);
			player.setBoard(this);
		}
	}

	/**
	 * Removes the player from the collection of players on this board. Also sets the player's board to null.
	 * @param player the player to remove from the player collection.
	 */
	public void removePlayer(Player player) {
		if (players.contains(player)) {
			players.remove(player);
		}
		player.setBoard(null);
	}

	/**
	 * Sets the active player. If the Player passed in was not in the board's
	 * PlayerCollection, it will be added.
	 * 
	 * @param player
	 */
	public void setActivePlayer(Player player) {
		if (!this.players.contains(player)) {
			this.players.add(player);
		}
		this.activePlayer = player;
	}

	/**
	 * Get the active player.
	 * 
	 * @return the active player.
	 */
	public Player getActivePlayer() {
		return this.activePlayer;
	}

	/**
	 * Set the active player to the next player in the list.
	 * 
	 * @return the new active player.
	 */
	public Player nextPlayer() {
		int currentPlayerIdx = this.players.indexOf(getActivePlayer());
		currentPlayerIdx = ++currentPlayerIdx % this.players.size();
		this.activePlayer = this.players.get(currentPlayerIdx);

		return this.getActivePlayer();
	}

	/**
	 * Executes the move passed in.
	 * 
	 * @param move
	 *            to be executed.
	 */
	public void doMove(ICommand move) {
		this.redoMoveStack.clear();
		this.moveStack.push(move).execute();
	}

	/**
	 * Undoes the most recently made move.
	 * 
	 * @return whether there are other moves that can be undone.
	 */
	public boolean undoMove() {
		if (this.moveStack.size() > 0) {
			// Pop the move from the moves stack, push it to the redo stack,
			// then undo the move.
			this.redoMoveStack.push(this.moveStack.pop()).undo();
		}
		return this.moveStack.size() > 0;
	}

	/**
	 * Undoes all moves up to and including the move passed in. If the move
	 * passed in is not a move that was previously made, this method will do
	 * nothing.
	 * 
	 * @param move
	 *            the last move to undo.
	 * @return whether the move was in the undo stack and was undone.
	 */
	public boolean undoMove(ICommand move) {
		if (!this.moveStack.contains(move)) {
			return false;
		}
		while (moveStack.size() > 0
				&& moveStack.peek() != move) {
			undoMove();
		}
		if (moveStack.size() > 0 && moveStack.peek() == move) {
			undoMove();
		}
		return true;
	}

	/**
	 * Makes the move that was most recently undone.
	 * 
	 * @return whether there are other moves that can be redone.
	 */
	public boolean redoMove() {
		if (this.redoMoveStack.size() > 0) {
			// Pop the move from the redo stack, push it to the moves stack,
			// then execute the move.
			this.moveStack.push(this.redoMoveStack.pop()).execute();
		}
		return this.redoMoveStack.size() > 0;
	}

	/**
	 * Redoes all moves up to and including the move passed in. If the move
	 * passed in is not a move that was previously undone, this method will do
	 * nothing.
	 * 
	 * @param move
	 *            the last move to be redone.
	 * @return whether the move was in the redo stack and was redone.
	 */
	public boolean redoMove(ICommand move) {
		if (!this.redoMoveStack.contains(move)) {
			return false;
		}
		while (moveStack.peek() != move) {
			redoMove();
		}
		return true;
	}

	/**
	 * Whether there are moves that can be undone.
	 * 
	 * @return whether there are moves that can be undone.
	 */
	public boolean canUndo() {
		return this.moveStack.size() > 0;
	}

	/**
	 * Whether there are moves that can be redone.
	 * 
	 * @return whether there are moves that can be redone.
	 */
	public boolean canRedo() {
		return this.redoMoveStack.size() > 0;
	}

	public boolean isLinear(Square square, Point start) {
		Point p = pointLookup.get( square );
		return p.x == start.x || p.y == start.y;
	}
}
